﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Reflection;

namespace Microsoft.BizTalk.Samples.EDIGuidance.HelperComponents
{
    public class XPathHelper
    {
        #region "Constructors

        public XPathHelper(string xpath)
            : base()
        {
            this._xpathGiven = xpath;
        }

        #endregion

        #region "Members"

        public delegate void XpathToCurrentNodeChangeHandler();
        private readonly string _nodeNameRegex = @"/\*\[local\-name\(\)='(?<NodeName>\w+) (\s and \s namespace\-uri='.*'\])?";
        private readonly string _nodeRegex = @"/\*\[local\-name\(\)='(?<NodeName>\w+) (\s and \s namespace\-uri='.*'\])?";
        private readonly string _nodeDelimiter = @"/*";

        private readonly RegexOptions _regexOptions = RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled;

        private int _totalNodeCount;

        public int TotalNodeCount
        {
            get { return this._totalNodeCount; }
        }

        private string _xpathGiven;

        public string XpathGiven
        {
            get { return this._xpathGiven; }
        }

        private string _xpathToCurrentNode;

        public string XpathToCurrentNode
        {
            get
            {
                // (02/12/2007, J.King): Added this conditional statement
                // as a means for a node to point to itself in case
                // the entire expath is stripped off and there's nothing left.
                if (string.IsNullOrEmpty(this._xpathToCurrentNode))
                {
                    // Point to yourself.
                    this._xpathToCurrentNode = ".";
                }
                return this._xpathToCurrentNode;
            }
        }

        private int _currentNodeCount;

        public int CurrentNodeCount
        {
            get { return _currentNodeCount; }
        }

        private string _currentNodeName;

        public string CurrentNodeName
        {
            get { return _currentNodeName = this.GetNodeName(this.XpathToCurrentNode); }
        }

        private string _formattedXpathGiven;

        public string FormattedXpathGiven
        {
            get { return _formattedXpathGiven; }
        }

        #endregion

        /// <summary>
        /// Prepares the fields of the XPathHelper object instance. 
        /// This method must be called before any fields or methods 
        /// are accessed or called, respectively.
        /// </summary>
        public void Bootstrap()
        {
            // Format xpath query given, set to FormattedXpath, XpathToCurrentNode
            this._formattedXpathGiven = this.FormatXpathQuery(this._xpathGiven);
            this._xpathToCurrentNode = this._formattedXpathGiven;

            // xpath to current node has changed, need to regroup
            this.Regroup();

            // Get total node count, current node count
            this._totalNodeCount = this._currentNodeCount;
        }

        /// <summary>
        /// Returns the node name for the last node in the given xpath.
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="xpath"/> is invalid.</exception>
        /// <param name="xpath">xpath</param>
        /// <returns>if a node was found, then the node name; otherwise empty string is returned.</returns>
        public string GetNodeName(string xpath)
        {
            //Format it just in case.
            xpath = this.FormatXpathQuery(xpath);

            Regex regex = new Regex(this._nodeNameRegex, this._regexOptions);

            string nodeName = string.Empty;

            MatchCollection mc = regex.Matches(xpath);

            if (mc.Count > 0)
            {
                // Get the last match, which represents the last node 
                // to which the given xpath points.
                nodeName = mc[mc.Count - 1].Groups["NodeName"].Value;
            }
            return nodeName;
        }

        /// <summary>
        /// Returns the number of nodes found within a given xapth after parsing it.
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="xpath"/> is invalid.</exception>
        /// <param name="xpath">xpath parsed</param>
        /// <returns>count</returns>
        public int GetNodeCount(string xpath)
        {
            // Format xpath just in case.
            xpath = this.FormatXpathQuery(xpath);

            // Find matches
            Regex regex = new Regex(this._nodeRegex, this._regexOptions);

            MatchCollection mc = regex.Matches(xpath);

            int nodeCount = (mc == null) ? 0 : mc.Count;

            return nodeCount;
        }

        /// <summary>
        /// Reformats an xpath query.
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="xpath"/> is invalid.</exception>
        /// <param name="xpath">xpath to format</param>
        /// <returns>formatted xpath </returns>
        public string FormatXpathQuery(string xpath)
        {
            #region "Predicates"

            // See if there is anything in there that look like []
            // if so the user probably formatted the string already
            if (xpath.IndexOf("[") > 0)
            {
                //xpath is already formatted...returning
                return xpath;
            }
            #endregion

            // Ok so there is no stuff in there like [local-name()= ...
            // But we need that for the XpathQuery to work,
            // so reformat the query by putting [local-name()= before every node
            // the user wants to select on.

            // Define the delimiter
            //string delimStr = "/";
            char[] delimiter = "/".ToCharArray();
            const string XpathNodeFormat = @"/*[local-name()='{0}']";
            string[] split = xpath.Split(delimiter);
            StringBuilder formattedXpath = new StringBuilder();

            // Enumerate all the strings
            foreach (string s in split)
            {
                // If Length > 0 reformat the xpath query
                if (s.Length > 0) { formattedXpath.AppendFormat(XpathNodeFormat, s); }
            }

            return formattedXpath.ToString();
        }

        /// <summary>
        /// Goes to the next to last node in the formatted xpath by truncating the last node off the xpath.
        /// </summary>
        public void GotoParent()
        {
            // Remove last node in the xpath
            if (this.XpathToCurrentNode != ".")
            {
                this._xpathToCurrentNode = this.StripOffLastNode(this._xpathToCurrentNode);

                // xpath to current node has changed, need to regroup
                this.Regroup();
            }
            else
            {
                //can't go any further, we're already at the top-most node
            }
        }

        /// <summary>
        /// Serves as a wrapper of processes that need to take place to ensure that 
        /// every field is "on the same page" by updating any stale data.
        /// </summary>
        private void Regroup()
        {
            // Set current node count.
            this._currentNodeCount = this.GetNodeCount(this._xpathToCurrentNode);

            // Set current node name
            this._currentNodeName = this.GetNodeName(this._xpathToCurrentNode);
        }

        /// <summary>
        /// Strips off the last node from the given xapth.
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="xpath"/> is invalid.</exception>
        /// <param name="xpath">xpath to parse</param>
        /// <returns>the same xpath but with the last node removed.</returns>
        private string StripOffLastNode(string xpath)
        {
            // Format it just in case.
            xpath = this.FormatXpathQuery(xpath);

            // Point to the beginning of the last node in the xpath.
            int i = xpath.LastIndexOf(this._nodeDelimiter);

            // Get everything but the last node.
            string newXpath = xpath.Substring(0, i);

            return newXpath;
        }
    }
}
